package com.mzw.service.impl;

import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mzw.bizmq.BiMessageProducer;
import com.mzw.common.ErrorCode;
import com.mzw.constant.CommonConstant;
import com.mzw.constant.ExecutionStatus;
import com.mzw.exception.BusinessException;
import com.mzw.exception.ThrowUtils;
import com.mzw.manager.AiManager;
import com.mzw.manager.RedisLimiterManager;
import com.mzw.mapper.ChartMapper;
import com.mzw.model.dto.chart.GenChartByAiRequest;
import com.mzw.model.entity.Chart;
import com.mzw.model.entity.User;
import com.mzw.model.vo.BiResponse;
import com.mzw.service.ChartService;
import com.mzw.service.EmailService;
import com.mzw.service.UserService;
import com.mzw.utils.ExcelUtils;
import com.mzw.utils.SendMessageUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;

/**
 *
 */
@Service
@Slf4j
public class ChartServiceImpl extends ServiceImpl<ChartMapper, Chart>
        implements ChartService {

    @Resource
    private UserService userService;

    @Resource
    private AiManager aiManager;

    @Resource
    private RedisLimiterManager redisLimiterManager;

    @Resource
    private ThreadPoolExecutor threadPoolExecutor;

    @Resource
    private ChartMapper chartMapper;

    @Resource
    private BiMessageProducer biMessageProducer;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private EmailService emailService;

    /**
     * 智能分析，同步
     * @param multipartFile       用户上传的文件信息
     * @param genChartByAiRequest 用户的需求
     * @param request             http request
     * @return
     */
    @Override
    public BiResponse genChartByAi(MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
        // 判断用户是否登录
        User loginUser = userService.getLoginUser(request);
        // 查看剩余调用次数
        Integer leftNum = loginUser.getLeftNum();
        ThrowUtils.throwIf(leftNum < 1, ErrorCode.PARAMS_ERROR, "剩余次数不足，请充值次数");
        // 用户输入数据
        // 图标名称
        String name = genChartByAiRequest.getName();
        // 用户需求
        String goal = genChartByAiRequest.getGoal();
        // 图表类型
        String chartType = genChartByAiRequest.getChartType();
        // 校验文件是否合法
        long size = multipartFile.getSize();
        String originalFilename = multipartFile.getOriginalFilename();
        // 校验文件大小
        final long ONE_MB = 1024 * 1024L;
        ThrowUtils.throwIf(size > ONE_MB, ErrorCode.PARAMS_ERROR, "文件超过 1M");
        // 校验文件后缀 aaa.png
        String suffix = FileUtil.getSuffix(originalFilename);
        // 定义合法的后缀列表
        final List<String> validFileSuffixList = Arrays.asList("xlsx");
        ThrowUtils.throwIf(!validFileSuffixList.contains(suffix), ErrorCode.PARAMS_ERROR, "文件后缀非法");

        // 限流判断，对每个用户进行限流
        redisLimiterManager.doRateLimit("genChartByAi_" + loginUser.getId());
        // 无需写 prompt，直接调用现有模型，下面是模型中设定好的prompt
//        final String prompt = "你是一个数据分析师和前端开发专家，接下来我会按照以下固定格式给你提供内容：\n" +
//                "分析需求：\n" +
//                "{数据分析的需求或者目标}\n" +
//                "原始数据：\n" +
//                "{csv格式的原始数据，用,作为分隔符}\n" +
//                "请根据这两部分内容，按照以下指定格式生成内容（此外不要输出任何多余的开头、结尾、注释）\n" +
//                "【【【【【\n" +
//                "{前端 Echarts V5 的 option 配置对象js代码，合理地将数据进行可视化，不要生成任何多余的内容，比如注释}\n" +
//                "【【【【【\n" +
//                "{明确的数据分析结论、越详细越好，不要生成多余的注释}";
        // 模型ID，根据ID去使用模型
        long biModelId = CommonConstant.BI_MODEL_ID;

        // 构造用户输入
        StringBuilder userInput = new StringBuilder();
        userInput.append("分析需求：").append("\n");

        // 拼接分析目标
        String userGoal = goal;
        if (StringUtils.isNotBlank(chartType)) {
            userGoal += "，请使用" + chartType;
        }
        userInput.append(userGoal).append("\n");
        userInput.append("原始数据：").append("\n");
        // 获取压缩后的数据
        String chartData = ExcelUtils.excelToCsv(multipartFile);
        userInput.append(chartData).append("\n");

        String result = aiManager.doChat(biModelId, userInput.toString());
        // 处理返回数据，只要结论与图表代码即可
        // 利用【【【【【 标记进行分割字符串
        String[] splits = result.split("【【【【【");
        // 判断Ai生成是否合理
        if (splits.length < 3) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "AI 生成错误");
        }
        // 获取结论和图标代码
        String genChart = splits[1].trim();
        String genResult = splits[2].trim();
        // 将生成后的相关数据插入到数据库
        Chart chart = new Chart();
        chart.setName(name);
        chart.setGoal(goal);
//        chart.setChartData(chartData);
        chart.setChartType(chartType);
        chart.setGenChart(genChart);
//        chart.setGenResult(genResult);
        chart.setUserId(loginUser.getId());
        chart.setStatus(ExecutionStatus.EXECUTION_STATUS_SUCCEED);
        boolean saveResult = this.save(chart);
        ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败");

        // 插入到自己的表中
        Long chartId = chart.getId();
        // sql = create table if not exists chart_%s(chartData text null comment '图表数据',)
        // comment '图表信息表' collate = utf8mb4_unicode_ci
        // 分表sql语句
        chartMapper.createTable(Long.toString(chartId));
        // 检查表是否创建成功
        String string = chartMapper.checkTableExists(Long.toString(chartId));
        ThrowUtils.throwIf(string == null, ErrorCode.SYSTEM_ERROR, "原始数据创建失败");
        // 保存数据
        // sql = "insert into chart_%s values('%s')"
        int insertNum = chartMapper.insertData(Long.toString(chartId), chartData, genResult);
        ThrowUtils.throwIf(!(insertNum > 0), ErrorCode.SYSTEM_ERROR, "原始数据保存失败");
        BiResponse biResponse = new BiResponse();
        biResponse.setGenChart(genChart);
        biResponse.setGenResult(genResult);
        biResponse.setChartId(chart.getId());
        // 生成图表成功，扣减调用次数。
        Long userId = loginUser.getId();
        ChartServiceImpl proxy = (ChartServiceImpl)AopContext.currentProxy();
        proxy.getLock(userId, chart);
        return biResponse;
    }

    /**
     * 基于线程池实现异步处理任务
     * @param multipartFile
     * @param genChartByAiRequest
     * @param request
     * @return
     */
    @Override
    public BiResponse genChartByAiAsync(MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
        User loginUser = userService.getLoginUser(request);
        // 是否还有调用次数
        int leftNum = loginUser.getLeftNum();
        ThrowUtils.throwIf(leftNum < 1, ErrorCode.PARAMS_ERROR, "剩余次数不足，请充值次数");
        String name = genChartByAiRequest.getName();
        String goal = genChartByAiRequest.getGoal();
        String chartType = genChartByAiRequest.getChartType();
        // 校验文件
        long size = multipartFile.getSize();
        String originalFilename = multipartFile.getOriginalFilename();
        // 校验文件大小
        final long ONE_MB = 1024 * 1024L;
        ThrowUtils.throwIf(size > ONE_MB, ErrorCode.PARAMS_ERROR, "文件超过 1M");
        // 校验文件后缀 aaa.png
        String suffix = FileUtil.getSuffix(originalFilename);
        final List<String> validFileSuffixList = Arrays.asList("xlsx", "xls");
        ThrowUtils.throwIf(!validFileSuffixList.contains(suffix), ErrorCode.PARAMS_ERROR, "文件后缀非法");

        // 限流判断，每个用户一个限流器
        redisLimiterManager.doRateLimit("genChartByAi_" + loginUser.getId());
        // 模型ID
        long biModelId = CommonConstant.BI_MODEL_ID;
        // 构造用户输入
        StringBuilder userInput = new StringBuilder();
        userInput.append("分析需求：").append("\n");

        // 拼接分析目标
        String userGoal = goal;
        if (StringUtils.isNotBlank(chartType)) {
            userGoal += "，请使用" + chartType;
        }
        userInput.append(userGoal).append("\n");
        userInput.append("原始数据：").append("\n");
        // 压缩后的数据
        String chartData = ExcelUtils.excelToCsv(multipartFile);
        userInput.append(chartData).append("\n");
        // 插入到数据库
        Chart chart = new Chart();
        chart.setName(name);
        chart.setGoal(goal);
//        chart.setChartData(chartData);
        chart.setChartType(chartType);
        // 将任务状态置为等待
        chart.setStatus(ExecutionStatus.EXECUTION_STATUS_WAIT);
        chart.setUserId(loginUser.getId());
        boolean saveResult = this.save(chart);
        ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败");

        // todo 建议处理任务队列满了后，抛异常的情况
        CompletableFuture.runAsync(() -> {
            // 先修改图表任务状态为 “执行中”。等执行成功后，修改为 “已完成”、保存执行结果；执行失败后，状态修改为 “失败”，记录任务失败信息。
            Chart updateChart = new Chart();
            updateChart.setId(chart.getId());
            // 此时将任务状态置为运行中
            updateChart.setStatus(ExecutionStatus.EXECUTION_STATUS_RUN);
            boolean b = this.updateById(updateChart);
            if (!b) {
                handleChartUpdateError(chart.getId(), "更新图表执行中状态失败");
                return;
            }
            // 调用 AI
            String result = aiManager.doChat(biModelId, userInput.toString());
            String[] splits = result.split("【【【【【");
            if (splits.length < 3) {
                handleChartUpdateError(chart.getId(), "AI 生成错误");
                return;
            }
            String genChart = splits[1].trim();
            String genResult = splits[2].trim();
            Chart updateChartResult = new Chart();
            updateChartResult.setId(chart.getId());
            updateChartResult.setGenChart(genChart);
//            updateChartResult.setGenResult(genResult);
            updateChartResult.setStatus(ExecutionStatus.EXECUTION_STATUS_SUCCEED);
            boolean updateResult = this.updateById(updateChartResult);
            if (!updateResult) {
                handleChartUpdateError(chart.getId(), "更新图表成功状态失败");
            }

            Long userId = loginUser.getId();
            ChartServiceImpl proxy = (ChartServiceImpl)AopContext.currentProxy();
            proxy.getLock(userId, chart);
            // 向用户发送消息任务执行成功
            SendMessageUtil.sendMessageSucceed(emailService, loginUser.getUserEmail());
        }, threadPoolExecutor);

        // 保存到数据库之后，直接将图表id返回
        BiResponse biResponse = new BiResponse();
        biResponse.setChartId(chart.getId());
        return biResponse;
    }

    @Override
    public BiResponse genChartByAiAsyncMq(MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
        User loginUser = userService.getLoginUser(request);
        Integer leftNum = loginUser.getLeftNum();
        ThrowUtils.throwIf(leftNum < 1, ErrorCode.PARAMS_ERROR, "剩余次数不足，请充值次数");
        String name = genChartByAiRequest.getName();
        String goal = genChartByAiRequest.getGoal();
        String chartType = genChartByAiRequest.getChartType();
        // 校验文件
        long size = multipartFile.getSize();
        String originalFilename = multipartFile.getOriginalFilename();
        // 校验文件大小
        final long ONE_MB = 1024 * 1024L;
        ThrowUtils.throwIf(size > ONE_MB, ErrorCode.PARAMS_ERROR, "文件超过 1M");
        // 校验文件后缀 aaa.png
        String suffix = FileUtil.getSuffix(originalFilename);
        final List<String> validFileSuffixList = Arrays.asList("xlsx", "xls");
        ThrowUtils.throwIf(!validFileSuffixList.contains(suffix), ErrorCode.PARAMS_ERROR, "文件后缀非法");
        // 限流判断，每个用户一个限流器
        redisLimiterManager.doRateLimit("genChartByAi_" + loginUser.getId());

        // 构造用户输入
        StringBuilder userInput = new StringBuilder();
        userInput.append("分析需求：").append("\n");

        // 拼接分析目标
        String userGoal = goal;
        if (StringUtils.isNotBlank(chartType)) {
            userGoal += "，请使用" + chartType;
        }
        userInput.append(userGoal).append("\n");
        userInput.append("原始数据：").append("\n");
        // 压缩后的数据
        String chartData = ExcelUtils.excelToCsv(multipartFile);
        userInput.append(chartData).append("\n");

        // 插入到数据库
        Chart chart = new Chart();
        chart.setName(name);
        chart.setGoal(goal);
//        chart.setChartData(chartData);
        chart.setChartType(chartType);
        chart.setStatus(ExecutionStatus.EXECUTION_STATUS_WAIT);
        chart.setUserId(loginUser.getId());
        boolean saveResult = this.save(chart);
        ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败");
        long newChartId = chart.getId();
        // 为了分表的操作，所以消息中携带用户原始数据
        String message = newChartId + "," + chartData;
//        biMessageProducer.sendMessage(String.valueOf(newChartId));
        // 将任务消息发送到消息队列中
        biMessageProducer.sendMessage(message);
        BiResponse biResponse = new BiResponse();
        biResponse.setChartId(newChartId);
        return biResponse;
    }

    @Override
    public void handleChartUpdateError(long chartId, String execMessage) {
        Chart updateChartResult = new Chart();
        updateChartResult.setId(chartId);
        updateChartResult.setStatus(ExecutionStatus.EXECUTION_STATUS_FAILED);
        updateChartResult.setExecMessage(execMessage);
        boolean updateResult = this.updateById(updateChartResult);
        SendMessageUtil.sendMessageFail(emailService,
                userService.getById(this.getById(chartId).getUserId()).getUserEmail(), execMessage);
        if (!updateResult) {
            log.error("更新图表失败状态失败" + chartId + "," + execMessage);
        }
    }
    @Transactional
    public  void getLock(Long userId, Chart chart) {
        RLock lock = redissonClient.getLock("lock:sub" + userId);
        try {
            if (lock.tryLock()) {
                // 在查询一次
                Integer leftNum_ = userService.getById(userId).getLeftNum();
                if (leftNum_ < 1) {
                    Chart updateNum = new Chart();
                    updateNum.setStatus(ExecutionStatus.EXECUTION_STATUS_FAILED);
                    updateNum.setId(chart.getId());
                    boolean update = this.updateById(updateNum);
                    if (!update) {
                        throw new BusinessException(ErrorCode.PARAMS_ERROR, "修改次数不足时状态失败");
                    }
                    handleChartUpdateError(chart.getId(), "剩余次数不足");
                    return;
                }
                UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
                updateWrapper.setSql("leftNum = leftNum - 1, totalNum = totalNum + 1");
                boolean isUpdate = userService.update(updateWrapper);
                if (!isUpdate) {
                    throw new BusinessException(ErrorCode.PARAMS_ERROR, "次数扣减失败");
                }
            }
        } catch (BusinessException e) {
            log.info("图表：{}，因为剩余次数不足失败",chart.getId());
        } finally {
            lock.unlock();
        }
    }
}




